Kickin My Macbook to the Curb

Filed under: Tech Stuff — Written by Chrissy on Thursday, February 22nd, 2007 @ 6:32 am

I believe I have a pretty high tolerance for inconveniences but I just couldn't take it: running Windows Server 2003 as a primary OS on Macbook blows.

As a fan of Unix, Windows and beautiful UIs, I promised myself I would put down any amount of money for a small Macbook the day it came out and I did just that. Then I ran back to work and attempted a Windows Server 2003 install. I never liked Windows XP and all the free crap it had to offer; I wanted a bare bones, unrestricted workstation and Win2k3 gave me that. It took about a week of hacking but I finally got the drivers installed. From there, my Macbook looked sexy and was super fast but ultimately became an utter inconvenience. As you can see from the sidebar, one of my most popular posts is about running Windows 2003 on a Macbook. I feel for those poor souls and can only hope they don't plan to use Win2k3 as their primary OS. As a friend said, "The Macbook is great. It's just not great for you."

So I've bought a new beauty and I love it, but before I get there, let me outline what sucks about running Win2k3 on a Macbook.

  • This applies to all Macbooks: inconsistent yet persistent fan issues. I don't care what you or you or you say, fans aren't supposed to sound that way. And it's not cool. Speaking of not cool, the Macbook gets way hot at times.

  • Remapping the keyboard to a Windows layout makes for a nasty delay after coming back from standby. The InputRemapper software from that guy works very well, I just don't like the wait.
  • If you want Bluetooth in Win2k3 to work, you have to run hid2hci every time it comes back from being on standby.
  • Built-in microphone jack + loud fans = impossible to make CBTs.
  • Power management sucks.
  • Will installing Leopard mess up my hacked 2k3 install? Will Parallels replace Bootcamp at some point, leaving me with no real option to boot Windows natively? I don't know but wondering causes stress.
  • No right click. I mapped the right Apple key to it but still.

So here I sit at Tommy's Joynt in San Francisco drinking coriander spiced beer and playing on my new Dell XPS M1210. I spent the past two weeks deciding between buying the super sexy but slower Sony Vaio, the equally sexy & slow new Fujitsu LED backlit Lifebook or this pretty-damn-affordable-and-unattractive-yet-lightweight-powerhouse from Dell. Dells just aren't pretty. I often wonder if Ford Motor outsources their car designers to Dell. Sure, you can make a laptop that looks like this..but why?

My new Dell lacks visual appeal (in my opinion) but makes up for it with its incredible amount of power. I've got a Core 2 Duo with 4MB of on-board cache (stylin' like a Xeon), 2 GB of RAM, 80GB 7200 RPM HDD and a 256MB nVidia card.. And to top it off, it's about 4.5lbs and retailed for about $2300 after tax, warranty, shipping and all that. Now I'm finally running Vista Enterprise with nothing but drivers, something I never really expected to do. But I really like it (also unexpected). "WOW" not so much but it's definitely a long overdue improvement. Here's my top favorite features so far

  • I don't need third party drivers to output my laptop display solely to my monitor. Finally!
  • The System Tray is cleaner and more informative
  • Vista Enteprise doesn't come with extra crap I don't need or want
  • Aero is really nice. I like how the taskbar now has popups to show you whats going on.
  • The search actually works. Searching in Windows, especially when searching inside files for text, was broken by Microsoft in XP and 2003. Even NT returns more accurate matches. How was this not noticed by engineers at Microsoft? I'm thinking it was a way to make people frustrated enough to install that Live Search bar.
  • Task scheduler and Event Viewer are super improved
  • The Windows Orb (formerly the Start button) is a big improvement over XP.
  • Dynamic disk resizing -- even on the windows partition

This page has a great outline of some of the new features and it also includes screenshots. Actually, the more tweaks and tricks I find, the more I realize that my initial list is going to change a ton over time. Something cool I just found is called "Additional Clocks" which allows you to add additional time information to the system tray clock hover. You can add up to 2 additional clocks.. I just added Louisiana and GMT.

A few things suck about Vista, including some networking issues but overall, I'm really digging both Vista and the Dell XPS M1210 I've named CRACKLIN. As for the Macbook, I've sold it to a Mac fan who can put it to better use.

MSSQL: Parse Search Engine Querystrings for Search Terms

Filed under: Quick Code, SQL Server — Written by Chrissy on Thursday, February 22nd, 2007 @ 1:15 am

As a compliment to my MaxMind GeoIP Locator scripts and the parse top-level domain from URL function, I've created another function to parse the search term from an URL.

I've heard that T-SQL is a bit slow with parsing strings and wanted to test this myself so I created this in both C# and T-SQL. The difference is very noticeable, indeed: while the original, unoptimized function I wrote in T-SQL took 11 seconds to parse 10,000 referral URLs, the C# version took 4 seconds. The updated, more efficient function takes 6 seconds to run in T-SQL and 3 seconds in C#. Below are two truncated versions of the functions I created. I slimmed them down to make them easier to read on the blog but you can download the full versions here: qscleaner.cs and qscleaner.sql. If you do not have Visual Studio.net, scroll to the bottom of the post -- there I link the qscleaner.dll and give sample CREATE ASSEMBLY syntax.

QSCleaner.cs

/*
Created by Chrissy LeMaire (clemaire@gmail.com)
Website: http://netnerds.net/
 
! - THIS IS THE TRUNCATED VERSION created to keep the blog posting size down. It works but not as
!- precisely. Please download the full version @ http://blog.netnerds.net/code/qscleaner.cs
 
This C# UDF does the following
1. Guesses the search engine's search keyword delimiter
2. Finds the term and extracts it
3. Replaces querystring percentage encoding with its human readable equiv.
 
NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
*/
 
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
 
public partial class UserDefinedFunctions
{
    [Microsoft.SqlServer.Server.SqlFunction]
    public static SqlString QSCleaner(string strURL)
    {
        int ampStart, slashStart, delimiterStart;
        string strQueryString = "";
        string strDelimiter = "";
 
        strURL = strURL.Replace("aq=", "aa=");
        strURL = strURL.Replace("as_q=", "aa=");
        strURL = strURL.Replace("as_oq=", "aa=");
        strURL = strURL.Replace("as_eq=", "aa=");
 
        while (strDelimiter.Length == 0)
        {
 
    // most used search delimiters
         if (strURL.IndexOf("q=") > 0)
        { strDelimiter = "q="; break; }
 
         if (strURL.IndexOf("p=") > 0)
        { strDelimiter = "p="; break; }
 
         if (strURL.IndexOf("query=") > 0)
        { strDelimiter = "="; break; }
 
         if (strURL.IndexOf("search=") > 0)
        { strDelimiter = "search="; break; }
 
         if (strURL.IndexOf("qry=") > 0)
        { strDelimiter = "qry="; break; }
 
         if (strURL.IndexOf("p=") > 0)
        { strDelimiter = "p="; break; }
 
         if (strURL.IndexOf("words=") > 0)
        { strDelimiter = "words="; break; }
 
         if (strURL.IndexOf("word=") > 0)
        { strDelimiter = "word="; break; }
 
         if (strURL.IndexOf("MT=") > 0)
        { strDelimiter = "MT="; break; }
 
         if (strURL.IndexOf("s=") > 0)
        { strDelimiter = "s="; break; }
 
         if (strURL.IndexOf("key=") > 0)
        { strDelimiter = "key="; break; }
        // end most used
 
                  //if by this time it isnt set.. it's not likely a search string
            if (strDelimiter.Length == 0) strDelimiter = "unknown";
        }
 
        if (strURL.IndexOf(strDelimiter) > 0)
        {
            try
            {
                delimiterStart = strURL.IndexOf(strDelimiter) + strDelimiter.Length;
                strQueryString = strURL.Substring(delimiterStart, (strURL.Length - delimiterStart));
                ampStart = strQueryString.IndexOf("&");
                if (ampStart > 0) strQueryString = strQueryString.Substring(0, ampStart);
 
                slashStart = strQueryString.IndexOf("/");
                if (slashStart > 0) strQueryString = strQueryString.Substring(0, slashStart - 1);
 
                // because I can't get System.Web.HttpUtility.UrlDecode to work
                strQueryString = strQueryString.Replace("%20", " ");
                strQueryString = strQueryString.Replace("%21", "!");
                strQueryString = strQueryString.Replace("%22", """);
                strQueryString = strQueryString.Replace("%23", "#");
                strQueryString = strQueryString.Replace("%24", "$");
                strQueryString = strQueryString.Replace("%25", "%");
                strQueryString = strQueryString.Replace("%26", "&");
                strQueryString = strQueryString.Replace("%27", "''");
                strQueryString = strQueryString.Replace("%28", "(");
                strQueryString = strQueryString.Replace("%29", ")");
                strQueryString = strQueryString.Replace("%2A", "*");
                strQueryString = strQueryString.Replace("%2B", "+");
                strQueryString = strQueryString.Replace("%2C", ",");
                strQueryString = strQueryString.Replace("%2D", "-");
                strQueryString = strQueryString.Replace("%2E", ".");
                strQueryString = strQueryString.Replace("%2F", "/");
                strQueryString = strQueryString.Replace("%3A", ":");
                strQueryString = strQueryString.Replace("%3B", ";");
                strQueryString = strQueryString.Replace("%3C", "<");
                strQueryString = strQueryString.Replace("%3D", "=");
                strQueryString = strQueryString.Replace("%3E", ">");
                strQueryString = strQueryString.Replace("%3F", "?");
                strQueryString = strQueryString.Replace("%40", "@");
                strQueryString = strQueryString.Replace("%5B", "[");
                strQueryString = strQueryString.Replace("%5C", "");
                strQueryString = strQueryString.Replace("%5D", "]");
                strQueryString = strQueryString.Replace("%5E", "^");
                strQueryString = strQueryString.Replace("%5F", "_");
                strQueryString = strQueryString.Replace("%60", "`");
                strQueryString = strQueryString.Replace("%7B", "{");
                strQueryString = strQueryString.Replace("%7C", "|");
                strQueryString = strQueryString.Replace("%7D", "}");
                strQueryString = strQueryString.Replace("+", " ");
            }
            catch
            {
                strQueryString = strURL;
            }
        }
        else
            strQueryString = strURL;
 
        return new SqlString(strQueryString);
    }
};

If you don't have access to SQL Server 2005 or SQL 2k5 CLR, you can still use a T-SQL function.

QSCleaner.sql

/*
Created by Chrissy LeMaire (clemaire@gmail.com)
Website: http://netnerds.net/
 
! - THIS IS THE TRUNCATED VERSION created to keep the blog posting size down. It works but not as
!- precisely. Please download the full version @ http://blog.netnerds.net/code/qscleaner.sql
 
This T-SQL Function does the following
1. Guesses the search engine's search keyword delimiter
2. Finds the term and extracts it
3. Replaces querystring percentage encoding with its human readable equiv.
 
NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
*/
 
CREATE FUNCTION [dbo].[QSCleaner] (@strURL varchar(512))
RETURNS varchar(255)
AS
BEGIN
DECLARE @ampStart int
DECLARE @queryStr varchar(512)
DECLARE @slashStart int
DECLARE @strDelimiter varchar(64)
 
--Clean up some stuff that gets in the way of q=
SET @strURL = REPLACE(@strURL, 'aq=', 'aa=')
SET @strURL = REPLACE(@strURL, 'as_q=', 'aa=')
SET @strURL = REPLACE(@strURL, 'as_oq=', 'aa=')
SET @strURL = REPLACE(@strURL, 'as_eq=', 'aa=')
 
--Get Delimiter
WHILE @strDelimiter IS NULL
BEGIN
    -- most used search delimiters
    IF CHARINDEX('q=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'q='
    BREAK
    END
 
    IF CHARINDEX('p=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'p='
    BREAK
    END
 
    IF CHARINDEX('query=',@strURL) > 0
    BEGIN
    SET @strDelimiter = '='
    BREAK
    END
 
    IF CHARINDEX('search=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'search='
    BREAK
    END
 
    IF CHARINDEX('qry=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'qry='
    BREAK
    END
 
    IF CHARINDEX('p=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'p='
    BREAK
    END
 
    IF CHARINDEX('words=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'words='
    BREAK
    END
 
    IF CHARINDEX('word=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'word='
    BREAK
    END
 
    IF CHARINDEX('MT=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'MT='
    BREAK
    END
 
    IF CHARINDEX('s=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 's='
    BREAK
    END
 
    IF CHARINDEX('key=',@strURL) > 0
    BEGIN
    SET @strDelimiter = 'key='
    BREAK
    END
    -- end most used
 
IF @strDelimiter IS NULL SET @strDelimiter = ''
END
 
  IF CHARINDEX(@strDelimiter,@strURL) > 0
  BEGIN
  SET  @queryStr = SUBSTRING(@strURL,CHARINDEX(@strDelimiter,@strURL)+LEN(@strDelimiter),LEN(@strURL))
  SET  @ampStart = CHARINDEX('&',@queryStr)
  IF @ampStart > 0 SET @queryStr = LEFT(@queryStr,@ampStart-1)
 
  SET  @slashStart = CHARINDEX('/',@queryStr)
  IF @slashStart > 0 SET @queryStr = LEFT(@queryStr,@slashStart-1)
  SET @queryStr = REPLACE(@queryStr, '%20', ' ')
  SET @queryStr = REPLACE(@queryStr, '%21', '!')
  SET @queryStr = REPLACE(@queryStr, '%22', '"')
  SET @queryStr = REPLACE(@queryStr, '%23', '#')
  SET @queryStr = REPLACE(@queryStr, '%24', '$')
  SET @queryStr = REPLACE(@queryStr, '%25', '%')
  SET @queryStr = REPLACE(@queryStr, '%26', '&')
  SET @queryStr = REPLACE(@queryStr, '%27', '''')
  SET @queryStr = REPLACE(@queryStr, '%28', '(')
  SET @queryStr = REPLACE(@queryStr, '%29', ')')
  SET @queryStr = REPLACE(@queryStr, '%2A', '*')
  SET @queryStr = REPLACE(@queryStr, '%2B', '+')
  SET @queryStr = REPLACE(@queryStr, '%2C', ',')
  SET @queryStr = REPLACE(@queryStr, '%2D', '-')
  SET @queryStr = REPLACE(@queryStr, '%2E', '.')
  SET @queryStr = REPLACE(@queryStr, '%2F', '/')
  SET @queryStr = REPLACE(@queryStr, '%3A', ':')
  SET @queryStr = REPLACE(@queryStr, '%3B', ';')
  SET @queryStr = REPLACE(@queryStr, '%3C', '<')
  SET @queryStr = REPLACE(@queryStr, '%3D', '=')
  SET @queryStr = REPLACE(@queryStr, '%3E', '>')
  SET @queryStr = REPLACE(@queryStr, '%3F', '?')
  SET @queryStr = REPLACE(@queryStr, '%40', '@')
  SET @queryStr = REPLACE(@queryStr, '%5B', '[')
  SET @queryStr = REPLACE(@queryStr, '%5C', '')
  SET @queryStr = REPLACE(@queryStr, '%5D', ']')
  SET @queryStr = REPLACE(@queryStr, '%5E', '^')
  SET @queryStr = REPLACE(@queryStr, '%5F', '_')
  SET @queryStr = REPLACE(@queryStr, '%60', '`')
  SET @queryStr = REPLACE(@queryStr, '%7B', '{')
  SET @queryStr = REPLACE(@queryStr, '%7C', '|')
  SET @queryStr = REPLACE(@queryStr, '%7D', '}')
  SET @queryStr = REPLACE(@queryStr, '+', ' ')
 
  END
if @queryStr is null SET @queryStr = @strURL
RETURN  @queryStr
END

If you do not have VS.NET but still wish to use the C# version of qscleaner, you can download the dll here: qscleaner.dll or you can email me for the SLN file and compile it yourself using msbuild which is included in the .NET 2.0 framework. Anyway, save the dll to the DATA directory that contains your .mdf files (for example: C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data). To "install" the UDF, run the following:

CREATE ASSEMBLY QSCleaner
FROM 'C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Data\qscleaner.dll'
WITH PERMISSION_SET = SAFE
GO
 
CREATE FUNCTION dbo.QSCleaner(@strURL nvarchar) RETURNS nvarchar
AS EXTERNAL NAME QSCleaner.UserDefinedFunctions.QSCleaner
GO

To call the function, create a query that's something like this: SELECT TOP 1000 dbo.qscleaner(referer) as keywords FROM hitcounter WHERE referer IS NOT NULL ORDER BY id DESC

Actually, I take it back. The above dll/assembly/function creation doesn't work -- I just receive 1000 h's all the way down. Compiling it via VS.NET works and the T-SQL function works but the manual creation does not. If anyone can shed some light, I'd appreciate it!

SQL Server 2005: SP2 Now Available for Download

Filed under: SQL Server, Tech Stuff — Written by Chrissy on Wednesday, February 21st, 2007 @ 1:28 am

You can now download SQL Server 2005 SP2 on Microsoft's website. Here's hoping that the install issues in SP1 won't plague SP2 as well!

MSSQL: The Differences Between SAFE, EXTERNAL_ACCESS and UNSAFE Assembly Permissions

Filed under: SQL Server, Tech Stuff — Written by Chrissy on Tuesday, February 20th, 2007 @ 7:16 pm

I found this useful security tip on Microsoft.com.

SAFE is the recommended permission setting for assemblies that perform computation and data management tasks without accessing resources outside an instance of SQL Server.

We recommend using EXTERNAL_ACCESS for assemblies that access resources outside of an instance of SQL Server. EXTERNAL_ACCESS assemblies include the reliability and scalability protections of SAFE assemblies, but from a security perspective are similar to UNSAFE assemblies. This is because code in EXTERNAL_ACCESS assemblies runs by default under the SQL Server service account and accesses external resources under that account, unless the code explicitly impersonates the caller. Therefore, permission to create EXTERNAL_ACCESS assemblies should be granted only to logins that are trusted to run code under the SQL Server service account. For more information about impersonation, see CLR Integration Security.

Specifying UNSAFE enables the code in the assembly complete freedom to perform operations in the SQL Server process space that can potentially compromise the robustness of SQL Server. UNSAFE assemblies can also potentially subvert the security system of either SQL Server or the common language runtime. UNSAFE permissions should be granted only to highly trusted assemblies. Only members of the sysadmin fixed server role can create and alter UNSAFE assemblies.

I wondered which was "worse", EXTERNAL_ACCESS or UNSAFE and now I know.. don't use UNSAFE unless absolutely necessary.

SQL Server 2005: Creating Your First C# CLR UDF in 10 Easy Steps (One of Which Includes Partying)

Filed under: Quick Code, SQL Server — Written by Chrissy on Wednesday, February 14th, 2007 @ 12:07 am

Well, 10 steps, give or take. This could have been executed in fewer steps had we not chosen such a useful function as an example <G>. This function, which is only supported in SQL Server 2005 with CLR enabled, esentially performs a reverse IP lookup (nslookup -type=ptr), uses System.Net.Dns.GetHostEntry and thus requires a bit more security configuration. So, let's begin.

1. In VS 2005, click File -> New -> Project -> Visual C# -> Database -> SQL Server Project -> OK.
2. Enter SQL Server name, credentials, and database name -> OK.
a. Accept the prompt which asks you if you want to enable CLR debugging in the database
3. In Solution Explorer, right click on the name of your SQL Server Project -> Add -> User Defined Function -> save it as "IPtoHostname.cs" (this will be the name of your function)
4. Copy and paste the following code into the IPtoHostname.cs window:

IPtoHostname.cs

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
 
public partial class UserDefinedFunctions
{
    [Microsoft.SqlServer.Server.SqlFunction]
    public static System.Data.SqlTypes.SqlString IPtoHostName(string ipaddr) // this is like T-SQL's CREATE FUNCTION dbo.IPtoHostName  (@strCCode varchar(xx))
    {
        try // This basically means "If the IP resolves properly and doesn't throw an exception, then..."
        {
            string myIP = System.Net.Dns.GetHostEntry(ipaddr).HostName.ToString();
            return new SqlString(myIP);
        }
        catch // "If it doesn't, just return the IP address"
        {
            return new SqlString(ipaddr);
        }
    }
};

Note: Again, because we use System.Net, we need to set some permissions which weakens the security of the given database. I haven't researched the implications of doing so but I figure if I'm going to run CLR stuff in my database, I'm willing to bend a little on security anyway.

5. Save your work. In Solution Explorer, right click on then name of your SQL Server Project and click Properties. Change the Permission Level to External.
6. Open SQL Server Management Studio and issue the following command, using the name of the database you selected in step 2.

EXEC sp_configure 'CLR ENABLED' , '1'
GO
RECONFIGURE
GO
ALTER DATABASE MyDBFromStep2 SET TRUSTWORTHY ON
GO

Note: In order to enable CLR or set the TRUSTWORTHYness of a database, it's likely that you must be a sys admin. And this is mostly for keywords to help Googlers... if you do not do the above, you will run into an error similiar to the following:

System.Security.SecurityException...

A .NET Framework error occurred during execution of user defined routine or aggregate 'IPtoHostName':
System.Security.SecurityException: Request for the permission of type 'System.Net.DnsPermission, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' failed.
System.Security.SecurityException:
   at System.Security.CodeAccessSecurityEngine.Check(Object demand, StackCrawlMark& stackMark, Boolean isPermSet)
   at System.Security.CodeAccessPermission.Demand()
   at System.Net.Dns.GetHostEntry(String hostNameOrAddress)
   at UserDefinedFunctions.IPtoHostName(String ipaddr)

7. Next, go back to Visual Studio, and select the "Debug" dropdown. Change that to "Release."
8. Save. Hit F5. You should have a successful build.
9. In SQL Management Studio, Query your database with the following statement:

select dbo.IPtoHostName('24.0.175.222') as hostname

See "c-24-0-175-222.hsd1.tx.comcast.net"
10. Laissez les bon temps rouler! (Party!)

Craptacular Microsoft Software Installs

Filed under: Tech Stuff, Windows — Written by Chrissy on Monday, February 12th, 2007 @ 7:17 pm

My friend Brandon messaged me this morning telling me that he ran into the same issues with Office 2007 Professional (which he won & subsequently downloaded from Microsoft's website). The Office 2007 file extraction locked up even though memory & cpu usage was next-to-nothing so, eventually, he cancelled out and his system became unstable and thus required a reboot. After the reboot, Brandon found the extracted files in %temp% and then ran it from there. He's is running Windows XP Professional with Office 2003 installed. A very common scenario, no? Then how come Microsoft never caught these lock-up problems before releasing the final version of Office 2k7? I know we're not the only ones -- ever since my previous post about my impossibly slow Office 2007 install, Googlers who are running into the same issues are finding my site while searching for a solution.

This reminds me of the crappy SQL Server 2005 SP1 install issues I ran into. Out of about five SQL Server machines, SQL2k5 SP1 installed successfully on only one. I spent a good two days trying to get the installs to work but they never did. Other people in newsgroups ran into the same problems and they were told by MVP's that the only thing the could do is wait until SQL2k5 SP2. It's not clear exactly what caused the problem but many of us seemed to have the "wrong" .Net framework installed or the wrong SQL Server Support tools versions. Two of my machines are production and I don't install frivolous versions of anything on there. Arrrg.. this reminds me of the old days when I'd spend a good week installing dependency after dependency for just one Linux package. Speaking of.. this popped into my mailbox this morning and gave me a good chuckle ;)

Active Directory: E-mail Notification for Newly Added Users and Computers

Filed under: Active Directory, Quick Code, VBScript — Written by Chrissy on Monday, February 12th, 2007 @ 8:26 am

Recently, someone asked if there was a way to be notified when servers have been added to an Active Directory domain. I looked around the Internet and it seems there's not a direct way to do this without some large software package. So in lieu of having an instant notification, I created a script that tallies up newly added user and computer accounts and emails the admin with all the info. This script is initially setup to be run once a day, but you can modify to whatever frequency you want.

I really expected this to take at most a couple hours to write but parsing through all the data turned out to take a heckofa lot of time. From converting the desired comparison date to UTC to parsing the AD attribute memberOf, I spent a good day working on this. I learned a lot, though and found this page which details the AD schema to be very helpful.

If you need anything more than this, you may want to consider an enterprise-type administration package such as Microsoft's MOM.

ADAddedUsersNComputers.vbs

'****************************************************************************
' This script created by Chrissy LeMaire (clemaire@gmail.com)
' Website: http://netnerds.net/
'
' ADAddedUsersNComputers.vbs
'
' This script Checks AD for any additions made to Users or Computers
' in the past 24 hours. The time interval to check can be changed below.
'
' NO WARRANTIES, USE THIS AT YOUR OWN RISK, etc.
'*****************************************************************************
 
'Please modify these four settings
strSMTPServer = "myexchangeserver"
strEmailFrom = "Administrator  <me@mydomain.com>"
strEmailTo = "Administrator <me@mydomain.com>"
 
strTimeInUTC = CompareDateUTCConvert("h",-24) 'This is the same syntax as dateAdd(). The example will get new users/computers added in the past 24 hours.
 
'Unless you want to change the domain to check or the format of the emailed info, nothing below really needs to be modified.
On Error Resume Next
numPersonCount = 0
numComputerCount = 0
 
Set objAdRootDSE = GetObject("LDAP://RootDSE")
Set objRS = CreateObject("adodb.recordset")
  varConfigNC = objAdRootDSE.Get("defaultNamingContext")
  strConnstring = "Provider=ADsDSOObject"
  strWQL = "SELECT ADsPath FROM 'LDAP://" & varConfigNC & "' WHERE createTimeStamp > '" & strTimeInUTC & "' and (objectCategory = 'Person' or objectCategory = 'Computer')"
 
  objRS.Open strWQL, strConnstring
      Do until objRS.eof
        Set objADUserOrComputer = GetObject(objRS.Fields.Item(0))
        strObjectCategory = ParseDN(objADUserOrComputer.objectCategory)
 
        Select Case strObjectCategory
        Case "Person"
        numPersonCount = numPersonCount + 1
            If Len(objADUserOrComputer.displayName) > 0 Then strUserMsg = strUserMsg & vbCrLf & "displayName = " & objADUserOrComputer.displayName
            'strUserMsg = strUserMsg & vbCrLf & "distinguishedName = " & objADUserOrComputer.distinguishedName
            strUserMsg = strUserMsg & vbCrLf & "sAMAccountName = " & objADUserOrComputer.sAMAccountName
            strUserMsg = strUserMsg & vbCrLf & "sAMAccountType = " & SAMAccountTypetoName(objADUserOrComputer.sAMAccountType)
            strUserMsg = strUserMsg & vbCrLf & "whenChanged = " & objADUserOrComputer.whenChanged
            strUserMsg = strUserMsg & vbCrLf & "whenCreated = " & objADUserOrComputer.whenCreated
            strUserGroups = ParseMemberOf(objADUserOrComputer.memberOf,objADUserOrComputer.PrimaryGroupID)
            strUserMsg = strUserMsg & vbCrLf & "Member Of: " & strUserGroups
            If Len(objADUserOrComputer.userPrincipalName) > 0 Then strUserMsg = strUserMsg & vbCrLf & "userPrincipalName = " & objADUserOrComputer.userPrincipalName
            strUserMsg = strUserMsg & vbCrLf
         Case "Computer"
         numComputerCount = numComputerCount + 1
            strCompMsg = strCompMsg & vbCrLf & "dNSHostName = " & objADUserOrComputer.dNSHostName
            strCompMsg = strCompMsg & vbCrLf & "isCriticalSystemObject = " & objADUserOrComputer.isCriticalSystemObject
            strCompMsg = strCompMsg & vbCrLf & "operatingSystem = " & objADUserOrComputer.operatingSystem
            strCompMsg = strCompMsg & vbCrLf & "operatingSystemServicePack = " & objADUserOrComputer.operatingSystemServicePack
            strCompMsg = strCompMsg & vbCrLf & "operatingSystemVersion = " & objADUserOrComputer.operatingSystemVersion
            If InStr(objADUserOrComputer.rIDSetReferences,"Domain Controller") > 0 Then strCompMsg = strCompMsg & vbCrLf & "Domain Controller = Yes"
            If Len(objADUserOrComputer.description) > 0 Then strCompMsg = strCompMsg & vbCrLf & "description = " & objADUserOrComputer.description
             If Len(objADUserOrComputer.machineRole) > 0 Then strCompMsg = strCompMsg & vbCrLf & "machineRole = " & objADUserOrComputer.machineRole
            If Len(objADUserOrComputer.physicalLocationObject) > 0 Then strCompMsg = strCompMsg & vbCrLf & "physicalLocationObject = " & ParseDN(objADUserOrComputer.physicalLocationObject)
            strCompMsg = strCompMsg & vbCrLf
         End Select
        objRS.movenext
        Set objADUserOrComputer = Nothing
      Loop
    objRS.close
Set objRS = Nothing
Set objAdRootDSE = Nothing
 
If Len(strUserMsg) > 0 Then strEmailMessage = strEmailMessage & "--------- USERS ---------" & vbCrLf & strUserMsg & vbCrLf
If Len(strCompMsg) > 0 Then strEmailMessage = strEmailMessage & "--------- COMPUTERS ---------" & vbCrLf & strCompMsg
If Len(strUserMsg) = 0 And Len(strCompMsg) = 0 Then strEmailMessage = "No users or computers have been added in the last 24 hours."
 
Set objCDO = CreateObject("CDO.Message")
    objCDO.Subject = "Users Added: " & numPersonCount & ". Computers Added: " & numComputerCount & "."
    objCDO.From = strEmailFrom
    objCDO.To = strEmailTo
    objCDO.TextBody = strEmailMessage
    objCDO.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2 'cdoSendUsingPort (1 = local, 3 = Exchange)
    objCDO.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = strSMTPServer
    objCDO.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
    objCDO.Configuration.Fields.Update
    objCDO.Send
    set objCDO = Nothing
 
Function CompareDateUTCConvert(dateAddInterval,compareNumber)
'Wow, this is a lil complex. So createTimestamp is in UTC format.
'So first we grab your machine's time bias and then apply it.
'Next, we adjust the date to the one you specified above (now()-24hours by default)
'Finally, we parse the final date to UTC format ie. 20070207032200.0Z
 
Set objSWbemServices = GetObject("winmgmts:" & "{impersonationLevel=impersonate}!\.\root\cimv2")
  Set colTimeZone = objSWbemServices.ExecQuery("SELECT * FROM Win32_TimeZone")
    For Each objTimeZone in colTimeZone
     strBias = objTimeZone.Bias
    Next
  Set colTimeZone = Nothing
Set objSWbemServices = Nothing
  strCompareDate = dateAdd(dateAddInterval,compareNumber,now())
  strUTCCompare = DateAdd("n",strBias*(-1),strCompareDate)
  CurrentUTC = Year(strUTCCompare) & Right("0" & Month(strUTCCompare),2) & Right("0" & Day(strUTCCompare),2)
  CurrentUTC = CurrentUTC & Right("0" & Hour(strUTCCompare),2) & Right("0" & Minute(strUTCCompare),2) & Right("0" & Second(strUTCCompare),2) & ".0Z"
  CompareDateUTCConvert = CurrentUTC
End Function
 
Function ParseDN(strDN)
'Take a DN and extract what we want then make it pretty.
  arrDN = split(strDN,",") 'CN=Example-Thing,CN=Whatever,CN=Etc
  strDN = right(arrDN(0),len(arrDN(0))-3) 'CN=Example-Thing -> Example-Thing
  strDN = replace(strDN,"-"," ") 'Example Thing
  ParseDN = strDN
End Function
 
Function ParseMemberOf(memberof,primarygroupid)
'This shows what groups a person belongs to.
'The output of memberof changes depending on
'how many groups the user is a member of, etc.
  Select Case TypeName(memberof)
    Case "String" ParseMemberOf = ParseDN(memberof)
    Case "Empty" ParseMemberOf = PrimaryGroupIDtoName(primarygroupid,varConfigNC)
    Case "Variant()"
      For each groupDN in memberof
         strUserGroups = strUserGroups & vbCrLf & ParseDN(groupDN)
      Next
      ParseMemberOf = strUserGroups
    Case Else ParseMemberOf = "Unknown"
  End Select
End Function
 
Function SAMAccountTypetoName(theType)
'Just makin it more useful...
  Select Case theType
    Case 268435456 SAMAccountTypetoName = "Group Object"
    Case 268435457 SAMAccountTypetoName = "Non-Security Group Object"
    Case 536870912 SAMAccountTypetoName = "Alias Object"
    Case 536870913 SAMAccountTypetoName = "Non-Security Alias Object"
    Case 805306368 SAMAccountTypetoName = "Normal User Account"
    Case 805306369 SAMAccountTypetoName = "Machine Account"
    Case 805306370 SAMAccountTypetoName = "Trust Account"
    Case 1073741824 SAMAccountTypetoName = "App Basic Group"
    Case 1073741825 SAMAccountTypetoName = "App Query Group"
    Case 2147483647 SAMAccountTypetoName = "Account Type Max"
    Case Else SAMAccountTypetoName = "Unknown"
  End Select
End Function
 
Function PrimaryGroupIDtoName(PGID,varConfigNC)
'Ugh.. the alternative to this function can be found here:
'http://support.microsoft.com/kb/297951
'both are kinda nasty.
  Set objRSPGID = CreateObject("adodb.recordset")
    Connstring = "Provider=ADsDSOObject"
    strSQL = "<ldap://" & varConfigNC & ">;(objectCategory=group);distinguishedName,primaryGroupToken,name;subtree"
     objRSPGID.Open strSQL, Connstring
      If not objRSPGID.eof and not objRSPGID.bof Then
        Do until objRSPGID.eof Or Len(strGroupName) > 0
        If PGID = objRSPGID("primaryGroupToken") Then strGroupName = objRSPGID("name")
         objRSPGID.movenext
        Loop
      End If
     objRSPGID.close
  Set objRSPGID = Nothing
  If Len(strGroupName) = 0 Then strGroupName = "Unknown"
  PrimaryGroupIDtoName =   strGroupName
End Function

To schedule this, save the above code as ADAddedUsersNComputers.vbs in C:\scripts (for ex.) and use Scheduled Tasks to run the following command: %windir%\system32\wscript.exe C:\scripts\ADAddedUsersNComputers.vbs. I suggest running it daily at the end of each workday.

CHM: Download Scripting Guy Archive Volume 1 (August 2004 - June 2006)

Filed under: Tech Stuff — Written by Chrissy on Saturday, February 10th, 2007 @ 8:47 am

I'm nearly a year behind on this but I just found the free 7MB CHM of the Scripting Guy archive which contains every published article from August 2004 to June 2006. This is a great compliment to the Windows IT Pro Master CD which is still 50%off (now $30).

AIM 6: Err.. Port 443 Isn't Encrypting Anything But the Initial Login

Filed under: Security, Tech Stuff — Written by Chrissy on Tuesday, February 6th, 2007 @ 2:06 am

A friend asked me what port AIM used and I guessed something along the lines of 5190. I wanted to check to make sure and, after issuing the command netstat -n from the comamnd prompt, I couldn't find any port even close to that in use. But I did see 443 in use.. SSL, eh? I was connected to the IP 205.188.10.248 at port 443. Using Sam Spade, I did an IP Block check and sure enough it was America Online.

UPDATE: Originally, I wrote the following:

This means that all communication between AOL's server and their AIM 6 chat client is very well encrypted -- great news for users who wish to use AIM in an environment where the latest (and most aggressive) version of Websense is running. Even though all of my outbound connections at work are encrypted by default, it's nice knowing that if I even accidently sign on with an insecure connection, my work-related, code-laden chats can't be sniffed (so suck it, Websense!).

After my post, however, two friends suggested that it's possible for AOL to just use port 443, not for SSL, but because it's open on nearlly all firewalls. I then decided to do additional research and after being told that Ethereal and Packetyzer were out-of-style, I downloaded Wireshark, sniffed my packets and found that only the initial login is encrypted. The rest of everything, nick lists, conversations, etc are all sent in clear-text :| So now back to square one.. make sure your connection is fully encrypted or you use an HTTPS AIM proxy if you want to chat it up on networks that employ Websense and other hardcore tracking software.

T-SQL: Don't Run This in a Production Environment

Filed under: Quick Code, SQL Server — Written by Chrissy on Friday, February 2nd, 2007 @ 10:15 am

Here's some code that I'm posting mainly for my future reference. I do not recommend running this anywhere near a production machine.

Quick Code

DECLARE @SQL  varchar(8000) -- if you use nvarchar for whack table names, change this to 4000
SET @SQL = ''
SELECT @SQL = @SQL + 'ALTER DATABASE ' + NAME + ' SET RECOVERY SIMPLE; ALTER DATABASE ' + NAME + ' SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DBCC SHRINKDATABASE (' + NAME + '); ALTER DATABASE ' + NAME + ' SET MULTI_USER' + Char(13)
FROM MASTER..Sysdatabases where sid != 0x01
 
PRINT @SQL
EXEC (@SQL)

This code does the following to all non-system databases:
1. Sets the recover to simple (my log files were OOC).
2. Kicks out all users and sets the db to single user
3. Shrinks the database files
4. Sets the database back to multi-user